using System;
using System.Text.RegularExpressions;

namespace ElveTouch
{
    // Found this library at http://www.codeproject.com/KB/recipes/commandlineparser.aspx

    // Formats:

    // Boolean:
    //  /help Toggle the state. When enrolled programmatically, this will set the state to true. 
    //  /help+ Set the Boolean to true (irrespective to its old state) 
    //  /help- Set the Boolean to false (irrespective to its old state)

    // Strings:
    //  /user:new-user-name 
    //  /user new-user-name 
    //  /user:"user name with spaces" 
    //  /user "user name with spaces"

    // Enum: (note that it is not case sensitive) 
    //  /day:Mon 
    //  /day:tue   
    //  /day sun




    /// <summary>Implementation of a command-line parsing class.  Is capable of
    /// having switches registered with it directly or can examine a registered
    /// class for any properties with the appropriate attributes appended to
    /// them.</summary>
    public class CommandLineParser
    {
        /// <summary>A simple internal class for passing back to the caller
        /// some information about the switch.  The internals/implementation
        /// of this class has privillaged access to the contents of the
        /// SwitchRecord class.</summary>
        public class SwitchInfo
        {
            #region Private Variables
            private object m_Switch = null;
            #endregion

            #region Public Properties
            public string Name { get { return (m_Switch as SwitchRecord).Name; } }
            public string Description { get { return (m_Switch as SwitchRecord).Description; } }
            public string[] Aliases { get { return (m_Switch as SwitchRecord).Aliases; } }
            public System.Type Type { get { return (m_Switch as SwitchRecord).Type; } }
            public object Value { get { return (m_Switch as SwitchRecord).Value; } }
            public object InternalValue { get { return (m_Switch as SwitchRecord).InternalValue; } }
            public bool IsEnum { get { return (m_Switch as SwitchRecord).Type.IsEnum; } }
            public string[] Enumerations { get { return (m_Switch as SwitchRecord).Enumerations; } }
            #endregion

            /// <summary>
            /// Constructor for the SwitchInfo class.  Note, in order to hide to the outside world
            /// information not necessary to know, the constructor takes a System.Object (aka
            /// object) as it's registering type.  If the type isn't of the correct type, an exception
            /// is thrown.
            /// </summary>
            /// <param name="rec">The SwitchRecord for which this class store information.</param>
            /// <exception cref="ArgumentException">Thrown if the rec parameter is not of
            /// the type SwitchRecord.</exception>
            public SwitchInfo(object rec)
            {
                if (rec is SwitchRecord)
                    m_Switch = rec;
                else
                    throw new ArgumentException();
            }
        }

        /// <summary>
        /// The SwitchRecord is stored within the parser's collection of registered
        /// switches.  This class is private to the outside world.
        /// </summary>
        private class SwitchRecord
        {
            #region Private Variables
            private string m_name = "";
            private string m_description = "";
            private object m_value = null;
            private System.Type m_switchType = typeof(bool);
            private System.Collections.ArrayList m_Aliases = null;
            private string m_Pattern = "";

            // The following advanced functions allow for callbacks to be
            // made to manipulate the associated data type.
            private System.Reflection.MethodInfo m_SetMethod = null;
            private System.Reflection.MethodInfo m_GetMethod = null;
            private object m_PropertyOwner = null;
            #endregion

            #region Private Utility Functions
            private void Initialize(string name, string description)
            {
                m_name = name;
                m_description = description;

                BuildPattern();
            }

            private void BuildPattern()
            {
                string matchString;
                if (Name.IndexOfAny(new char[] { '^', '$', '.', '|', '*', '+', '?' }) >= 0) // there are more, but i think '?' is the only one that will be used.
                    matchString = "\\" + Name;
                else
                    matchString = Name;

                if (Aliases != null && Aliases.Length > 0)
                {
                    foreach (string s in Aliases)
                    {
                        if (s.IndexOfAny(new char[]{'^', '$', '.', '|', '*', '+', '?'}) >= 0) // there are more, but i think '?' is the only one that will be used.
                            matchString += "|\\" + s;
                        else
                            matchString += "|" + s;
                        
                    }
                }

                string strPatternStart = @"(\s|^)(?<match>(-{1,2}|/)(";
                string strPatternEnd;  // To be defined below.

                // The common suffix ensures that the switches are followed by
                // a white-space OR the end of the string.  This will stop
                // switches such as /help matching /helpme
                //
                string strCommonSuffix = @"(?=(\s|$))";

                if (Type == typeof(bool))
                    strPatternEnd = @")(?<value>(\+|-){0,1}))";
                else if (Type == typeof(string))
                    // John H. - I changed the regex pattern below since the original didn't handle quoted strings well.
                    //strPatternEnd = @")(?::|\s+))((?:"")(?<value>.+)(?:"")|(?<value>\S+))";
                    strPatternEnd = @")(?::|\s+))(?<value>[^\s""]+|""(?<value>[^""]*)"")";
                else if (Type == typeof(int))
                    strPatternEnd = @")(?::|\s+))((?<value>(-|\+)[0-9]+)|(?<value>[0-9]+))";
                else if (Type.IsEnum)
                {
                    string[] enumNames = Enumerations;
                    string e_str = enumNames[0];
                    for (int e = 1; e < enumNames.Length; e++)
                        e_str += "|" + enumNames[e];
                    strPatternEnd = @")(?::|\s+))(?<value>" + e_str + @")";
                }
                else
                    throw new System.ArgumentException();

                // Set the internal regular expression pattern.
                m_Pattern = strPatternStart + matchString + strPatternEnd + strCommonSuffix;
            }
            #endregion

            #region Public Properties
            public object Value
            {
                get
                {
                    if (ReadValue != null)
                        return ReadValue;
                    else
                        return m_value;
                }
            }

            public object InternalValue
            {
                get { return m_value; }
            }

            public string Name
            {
                get { return m_name; }
                set { m_name = value; }
            }

            public string Description
            {
                get { return m_description; }
                set { m_description = value; }
            }

            public System.Type Type
            {
                get { return m_switchType; }
            }

            public string[] Aliases
            {
                get { return (m_Aliases != null) ? (string[])m_Aliases.ToArray(typeof(string)) : null; }
            }

            public string Pattern
            {
                get { return m_Pattern; }
            }

            public System.Reflection.MethodInfo SetMethod
            {
                set { m_SetMethod = value; }
            }

            public System.Reflection.MethodInfo GetMethod
            {
                set { m_GetMethod = value; }
            }

            public object PropertyOwner
            {
                set { m_PropertyOwner = value; }
            }

            public object ReadValue
            {
                get
                {
                    object o = null;
                    if (m_PropertyOwner != null && m_GetMethod != null)
                        o = m_GetMethod.Invoke(m_PropertyOwner, null);
                    return o;
                }
            }

            public string[] Enumerations
            {
                get
                {
                    if (m_switchType.IsEnum)
                        return System.Enum.GetNames(m_switchType);
                    else
                        return null;
                }
            }
            #endregion

            #region Constructors
            public SwitchRecord(string name, string description)
            {
                Initialize(name, description);
            }

            public SwitchRecord(string name, string description, System.Type type)
            {
                if (type == typeof(bool) ||
                      type == typeof(string) ||
                      type == typeof(int) ||
                      type.IsEnum)
                {
                    m_switchType = type;
                    Initialize(name, description);
                }
                else
                    throw new ArgumentException("Currently only Ints, Bool and Strings are supported");
            }
            #endregion

            #region Public Methods
            public void AddAlias(string alias)
            {
                if (m_Aliases == null)
                    m_Aliases = new System.Collections.ArrayList();
                m_Aliases.Add(alias);

                BuildPattern();
            }

            public void Notify(object value)
            {
                if (m_PropertyOwner != null && m_SetMethod != null)
                {
                    object[] parameters = new object[1];
                    parameters[0] = value;
                    m_SetMethod.Invoke(m_PropertyOwner, parameters);
                }
                m_value = value;
            }

            #endregion
        }


        #region Private Variables
        private string m_commandLine = "";
        private string m_workingString = "";
        private string m_applicationName = "";
        private string[] m_splitParameters = null;
        private System.Collections.ArrayList m_switches = null;
        #endregion

        #region Private Utility Functions
        private void ExtractApplicationName()
        {
            // John H. - The original regex here is wrong since it can't deal with spaces in the application path or name.
            // wrong --> Regex r = new Regex(@"^(?<commandLine>("".+""|(\S)+))(?<remainder>.+)", RegexOptions.ExplicitCapture);
            Regex r = new Regex(@"^(?<commandLine>[^\s""]+|""([^""]*)"")(?<remainder>.+)", RegexOptions.ExplicitCapture); // I updated the regex here to work.
            Match m = r.Match(m_commandLine);
            if (m != null && m.Groups["commandLine"] != null)
            {
                m_applicationName = m.Groups["commandLine"].Value;
                m_workingString = m.Groups["remainder"].Value;
            }
        }

        private void SplitParameters()
        {
            // Populate the split parameters array with the remaining parameters.
            // Note that if quotes are used, the quotes are removed.
            // e.g.   one two three "four five six"
            //						0 - one
            //						1 - two
            //						2 - three
            //						3 - four five six
            // (e.g. 3 is not in quotes).
            Regex r = new Regex(@"((\s*(""(?<param>.+?)""|(?<param>\S+))))",
                RegexOptions.ExplicitCapture);
            MatchCollection m = r.Matches(m_workingString);

            if (m != null)
            {
                m_splitParameters = new string[m.Count];
                for (int i = 0; i < m.Count; i++)
                    m_splitParameters[i] = m[i].Groups["param"].Value;
            }
        }

        private void HandleSwitches()
        {
            if (m_switches != null)
            {
                foreach (SwitchRecord s in m_switches)
                {
                    Regex r = new Regex(s.Pattern,
                        RegexOptions.ExplicitCapture
                        | RegexOptions.IgnoreCase);
                    MatchCollection m = r.Matches(m_workingString);
                    if (m != null)
                    {
                        for (int i = 0; i < m.Count; i++)
                        {
                            string value = null;
                            if (m[i].Groups != null && m[i].Groups["value"] != null)
                                value = m[i].Groups["value"].Value;


                            if (s.Type == typeof(bool))
                            {
                                bool state = true;
                                // The value string may indicate what value we want.
                                if (m[i].Groups != null && m[i].Groups["value"] != null)
                                {
                                    switch (value)
                                    {
                                        case "+": state = true;
                                            break;
                                        case "-": state = false;
                                            break;
                                        case "": if (s.ReadValue != null)
                                                state = !(bool)s.ReadValue;
                                            break;
                                        default: break;
                                    }
                                }
                                s.Notify(state);
                                break;
                            }
                            else if (s.Type == typeof(string))
                            {
                                // John H. - Since I added the new regex we need to strip any bounding quotes.
                                if (value.Length > 1 && value[0] == '\"' && value[value.Length - 1] == '\"')
                                    value = value.Substring(1, value.Length - 2);
                                s.Notify(value);
                            }
                            else if (s.Type == typeof(int))
                                s.Notify(int.Parse(value));
                            else if (s.Type.IsEnum)
                                s.Notify(System.Enum.Parse(s.Type, value, true));
                        }
                    }

                    m_workingString = r.Replace(m_workingString, " ");
                }
            }
        }

        #endregion

        #region Public Properties
        public string ApplicationName
        {
            get { return m_applicationName; }
        }

        public string[] Parameters
        {
            get { return m_splitParameters; }
        }

        public SwitchInfo[] Switches
        {
            get
            {
                if (m_switches == null)
                    return null;
                else
                {
                    SwitchInfo[] si = new SwitchInfo[m_switches.Count];
                    for (int i = 0; i < m_switches.Count; i++)
                        si[i] = new SwitchInfo(m_switches[i]);
                    return si;
                }
            }
        }

        public object this[string name]
        {
            get
            {
                if (m_switches != null)
                    for (int i = 0; i < m_switches.Count; i++)
                        if (string.Compare((m_switches[i] as SwitchRecord).Name, name, true) == 0)
                            return (m_switches[i] as SwitchRecord).Value;
                return null;
            }
        }

        /// <summary>This function returns a list of the unhandled switches
        /// that the parser has seen, but not processed.</summary>
        /// <remark>The unhandled switches are not removed from the remainder
        /// of the command-line.</remark>
        public string[] UnhandledSwitches
        {
            get
            {
                string switchPattern = @"(\s|^)(?<match>(-{1,2}|/)(.+?))(?=(\s|$))";
                Regex r = new Regex(switchPattern,
                    RegexOptions.ExplicitCapture
                    | RegexOptions.IgnoreCase);
                MatchCollection m = r.Matches(m_workingString);

                if (m != null)
                {
                    string[] unhandled = new string[m.Count];
                    for (int i = 0; i < m.Count; i++)
                        unhandled[i] = m[i].Groups["match"].Value;
                    return unhandled;
                }
                else
                    return null;
            }
        }
        #endregion

        #region Public Methods
        public void AddSwitch(string name, string description)
        {
            if (m_switches == null)
                m_switches = new System.Collections.ArrayList();

            SwitchRecord rec = new SwitchRecord(name, description);
            m_switches.Add(rec);
        }

        public void AddSwitch(string[] names, string description)
        {
            if (m_switches == null)
                m_switches = new System.Collections.ArrayList();
            SwitchRecord rec = new SwitchRecord(names[0], description);
            for (int s = 1; s < names.Length; s++)
                rec.AddAlias(names[s]);
            m_switches.Add(rec);
        }

        public bool Parse()
        {
            ExtractApplicationName();

            // Remove switches and associated info.
            HandleSwitches();

            // Split parameters.
            SplitParameters();

            return true;
        }

        public object InternalValue(string name)
        {
            if (m_switches != null)
                for (int i = 0; i < m_switches.Count; i++)
                    if (string.Compare((m_switches[i] as SwitchRecord).Name, name, true) == 0)
                        return (m_switches[i] as SwitchRecord).InternalValue;
            return null;
        }
        #endregion

        #region Constructors
        public CommandLineParser(string commandLine)
        {
            m_commandLine = commandLine;
        }

        public CommandLineParser(string commandLine,
                            object classForAutoAttributes)
        {
            m_commandLine = commandLine;

            Type type = classForAutoAttributes.GetType();
            System.Reflection.MemberInfo[] members = type.GetMembers();

            for (int i = 0; i < members.Length; i++)
            {
                object[] attributes = members[i].GetCustomAttributes(false);
                if (attributes.Length > 0)
                {
                    SwitchRecord rec = null;

                    foreach (Attribute attribute in attributes)
                    {
                        if (attribute is CommandLineSwitchAttribute)
                        {
                            CommandLineSwitchAttribute switchAttrib =
                                (CommandLineSwitchAttribute)attribute;

                            // Get the property information.  We're only handling
                            // properties at the moment!
                            if (members[i] is System.Reflection.PropertyInfo)
                            {
                                System.Reflection.PropertyInfo pi = (System.Reflection.PropertyInfo)members[i];

                                rec = new SwitchRecord(switchAttrib.Name,
                                                                switchAttrib.Description,
                                                                pi.PropertyType);

                                // Map in the Get/Set methods.
                                rec.SetMethod = pi.GetSetMethod();
                                rec.GetMethod = pi.GetGetMethod();
                                rec.PropertyOwner = classForAutoAttributes;

                                // Can only handle a single switch for each property
                                // (otherwise the parsing of aliases gets silly...)
                                break;
                            }
                        }
                    }

                    // See if any aliases are required.  We can only do this after
                    // a switch has been registered and the framework doesn't make
                    // any guarantees about the order of attributes, so we have to
                    // walk the collection a second time.
                    if (rec != null)
                    {
                        foreach (Attribute attribute in attributes)
                        {
                            if (attribute is CommandLineAliasAttribute)
                            {
                                CommandLineAliasAttribute aliasAttrib =
                                    (CommandLineAliasAttribute)attribute;
                                rec.AddAlias(aliasAttrib.Alias);
                            }
                        }
                    }

                    // Assuming we have a switch record (that may or may not have
                    // aliases), add it to the collection of switches.
                    if (rec != null)
                    {
                        if (m_switches == null)
                            m_switches = new System.Collections.ArrayList();
                        m_switches.Add(rec);
                    }
                }
            }
        }
        #endregion

        public void ShowHelp()
        {
            // Show App Name
            Console.Write(System.IO.Path.GetFileNameWithoutExtension(this.ApplicationName.Replace("\"", "")).ToUpper() + " ");

            // Show arguments
            foreach (CommandLineParser.SwitchInfo theSwitch in this.Switches)
            {
                if (theSwitch.Type == typeof(bool))
                    Console.Write(" [/" + theSwitch.Name + "]");
                else
                    Console.Write(" [/" + theSwitch.Name + ":value]");
            }
            Console.WriteLine();
            Console.WriteLine();

            // Show Argument Descriptions
            foreach (CommandLineParser.SwitchInfo theSwitch in this.Switches)
            {
                Console.Write("  /" + theSwitch.Name + "    " + theSwitch.Description);
                if (theSwitch.IsEnum)
                {
                    Console.Write("  ");
                    for (int i = 0; i < theSwitch.Enumerations.Length; i++)
                    {
                        Console.Write(theSwitch.Enumerations[i]);
                        if (i < theSwitch.Enumerations.Length - 1)
                            Console.Write(", ");
                    }
                }
                Console.WriteLine();
            }
        }
    }

    /// <summary>Implements a basic command-line switch by taking the
    /// switching name and the associated description.</summary>
    /// <remark>Only currently is implemented for properties, so all
    /// auto-switching variables should have a get/set method supplied.</remark>
    [AttributeUsage(AttributeTargets.Property)]
    public class CommandLineSwitchAttribute : System.Attribute
    {
        #region Private Variables
        private string m_name = "";
        private string m_description = "";
        #endregion

        #region Public Properties
        /// <summary>Accessor for retrieving the switch-name for an associated
        /// property.</summary>
        public string Name { get { return m_name; } }

        /// <summary>Accessor for retrieving the description for a switch of
        /// an associated property.</summary>
        public string Description { get { return m_description; } }

        #endregion

        #region Constructors
        /// <summary>Attribute constructor.</summary>
        public CommandLineSwitchAttribute(string name,
                                                      string description)
        {
            m_name = name;
            m_description = description;
        }
        #endregion
    }

    /// <summary>
    /// This class implements an alias attribute to work in conjunction
    /// with the <see cref="CommandLineSwitchAttribute">CommandLineSwitchAttribute</see>
    /// attribute.  If the CommandLineSwitchAttribute exists, then this attribute
    /// defines an alias for it.
    /// </summary>
    [AttributeUsage(AttributeTargets.Property)]
    public class CommandLineAliasAttribute : System.Attribute
    {
        #region Private Variables
        protected string m_Alias = "";
        #endregion

        #region Public Properties
        public string Alias
        {
            get { return m_Alias; }
        }

        #endregion

        #region Constructors
        public CommandLineAliasAttribute(string alias)
        {
            m_Alias = alias;
        }
        #endregion
    }
}
